Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

365
Vistas
¿Por qué "1000000000000000 en el rango (1000000000000001)" es tan rápido en Python 3?

Tengo entendido que la función range() , que en realidad es un tipo de objeto en Python 3 , genera su contenido sobre la marcha, de forma similar a un generador.

Siendo este el caso, habría esperado que la siguiente línea tomara una cantidad de tiempo excesiva porque, para determinar si 1 cuatrillón está en el rango, se tendría que generar un cuatrillón de valores:

 1_000_000_000_000_000 in range(1_000_000_000_000_001)

Además: parece que no importa cuántos ceros agregue, el cálculo lleva más o menos la misma cantidad de tiempo (básicamente instantáneo).

También probé cosas como esta, pero el cálculo sigue siendo casi instantáneo:

 # count by tens 1_000_000_000_000_000_000_000 in range(0,1_000_000_000_000_000_000_001,10)

Si trato de implementar mi propia función de rango, ¡el resultado no es tan bueno!

 def my_crappy_range(N): i = 0 while i < N: yield i i += 1 return

¿Qué hace el objeto range() debajo del capó que lo hace tan rápido?


La respuesta de Martijn Pieters fue elegida por su integridad, pero también vea la primera respuesta de abarnert para una buena discusión de lo que significa que el range sea una secuencia completa en Python 3, y alguna información/advertencia sobre la posible inconsistencia para la optimización de la función __contains__ en Python implementaciones. La otra respuesta de abarnert entra en más detalles y proporciona enlaces para aquellos interesados en la historia detrás de la optimización en Python 3 (y la falta de optimización de xrange en Python 2). Las respuestas por poke y por wim proporcionan el código fuente C relevante y las explicaciones para aquellos que estén interesados.

over 4 years ago · Santiago Trujillo
9 Respuestas
Responde la pregunta

0

Para agregar a la respuesta de Martijn, esta es la parte relevante de la fuente (en C, ya que el objeto de rango está escrito en código nativo):

 static int range_contains(rangeobject *r, PyObject *ob) { if (PyLong_CheckExact(ob) || PyBool_Check(ob)) return range_contains_long(r, ob); return (int)_PySequence_IterSearch((PyObject*)r, ob, PY_ITERSEARCH_CONTAINS); }

Entonces, para los objetos PyLong (que es int en Python 3), usará la función range_contains_long para determinar el resultado. Y esa función esencialmente verifica si ob está en el rango especificado (aunque parece un poco más complejo en C).

Si no es un objeto int , recurre a la iteración hasta que encuentra el valor (o no).

Toda la lógica podría traducirse a pseudo-Python así:

 def range_contains (rangeObj, obj): if isinstance(obj, int): return range_contains_long(rangeObj, obj) # default logic by iterating return any(obj == x for x in rangeObj) def range_contains_long (r, num): if r.step > 0: # positive step: r.start <= num < r.stop cmp2 = r.start <= num cmp3 = num < r.stop else: # negative step: r.start >= num > r.stop cmp2 = num <= r.start cmp3 = r.stop < num # outside of the range boundaries if not cmp2 or not cmp3: return False # num must be on a valid step inside the boundaries return (num - r.start) % r.step == 0
over 4 years ago · Santiago Trujillo Denunciar

0

El malentendido fundamental aquí es pensar que el range es un generador. Que no es. De hecho, no es ningún tipo de iterador.

Puedes decir esto con bastante facilidad:

 >>> a = range(5) >>> print(list(a)) [0, 1, 2, 3, 4] >>> print(list(a)) [0, 1, 2, 3, 4]

Si fuera un generador, iterarlo una vez lo agotaría:

 >>> b = my_crappy_range(5) >>> print(list(b)) [0, 1, 2, 3, 4] >>> print(list(b)) []

Lo que realmente es el range es una secuencia, como una lista. Incluso puedes probar esto:

 >>> import collections.abc >>> isinstance(a, collections.abc.Sequence) True

Esto significa que tiene que seguir todas las reglas de ser una secuencia:

 >>> a[3] # indexable 3 >>> len(a) # sized 5 >>> 3 in a # membership True >>> reversed(a) # reversible <range_iterator at 0x101cd2360> >>> a.index(3) # implements 'index' 3 >>> a.count(3) # implements 'count' 1

La diferencia entre un range y una list es que un range es una secuencia perezosa o dinámica ; no recuerda todos sus valores, solo recuerda su start , stop y step , y crea los valores a pedido en __getitem__ .

(Como nota al margen, si print(iter(a)) , notará que range usa el mismo tipo de listiterator que list . ¿Cómo funciona eso? Un listiterator no usa nada especial sobre list excepto por el hecho de que proporciona una implementación en C de __getitem__ , por lo que también funciona bien para el range ).


Ahora, no hay nada que diga que Sequence.__contains__ tiene que ser un tiempo constante; de hecho, para ejemplos obvios de secuencias como list , no lo es. Pero no hay nada que diga que no puede ser. Y es más fácil implementar range.__contains__ para simplemente verificarlo matemáticamente ( (val - start) % step , pero con cierta complejidad adicional para lidiar con los pasos negativos) que generar y probar todos los valores, entonces, ¿por qué no debería hacerlo? es la mejor manera?

Pero no parece haber nada en el idioma que garantice que esto suceda. Como señala Ashwini Chaudhari, si le asigna un valor no integral, en lugar de convertirlo a un número entero y hacer la prueba matemática, volverá a iterar todos los valores y compararlos uno por uno. Y solo porque las versiones CPython 3.2+ y PyPy 3.x contienen esta optimización, y es una buena idea obvia y fácil de hacer, no hay razón por la que IronPython o NewKickAssPython 3.x no puedan omitirla. (Y, de hecho, CPython 3.0-3.1 no lo incluyó).


Si range en realidad fuera un generador, como my_crappy_range , entonces no tendría sentido probar __contains__ de esta manera, o al menos la forma en que tiene sentido no sería obvia. Si ya ha iterado los primeros 3 valores, ¿ 1 todavía está in el generador? ¿Debería la prueba de 1 hacer que itere y consuma todos los valores hasta 1 (o hasta el primer valor >= 1 )?

over 4 years ago · Santiago Trujillo Denunciar

0

Las otras respuestas ya lo explicaron bien, pero me gustaría ofrecer otro experimento que ilustre la naturaleza de los objetos de rango:

 >>> r = range(5) >>> for i in r: print(i, 2 in r, list(r)) 0 True [0, 1, 2, 3, 4] 1 True [0, 1, 2, 3, 4] 2 True [0, 1, 2, 3, 4] 3 True [0, 1, 2, 3, 4] 4 True [0, 1, 2, 3, 4]

Como puede ver, un objeto de range es un objeto que recuerda su rango y se puede usar muchas veces (incluso mientras se itera sobre él), no solo un generador de una sola vez.

over 4 years ago · Santiago Trujillo Denunciar

0

Si se pregunta por qué se agregó esta optimización a range.__contains__ y por qué no se agregó a xrange.__contains__ en 2.7:

Primero, como descubrió Ashwini Chaudhary, el problema 1766304 se abrió explícitamente para optimizar [x]range.__contains__ . Se aceptó un parche para esto y se registró para 3.2 , pero no se retroportó a 2.7 porque " xrange se ha comportado así durante tanto tiempo que no veo qué nos compra para enviar el parche tan tarde". (2.7 estaba casi agotado en ese momento).

Mientras tanto:

Originalmente, xrange era un objeto no del todo secuencial. Como dicen los documentos 3.1 :

Los objetos de rango tienen muy poco comportamiento: solo admiten la indexación, la iteración y la función len .

Esto no era del todo cierto; un objeto xrange en realidad admitía algunas otras cosas que vienen automáticamente con la indexación y len , * incluido __contains__ (a través de la búsqueda lineal). Pero nadie pensó que valía la pena hacerlos secuencias completas en ese momento.

Luego, como parte de la implementación del PEP de clases base abstractas , era importante determinar qué tipos integrados deberían marcarse como implementadores de qué ABC, y xrange / range afirmó implementar collections.Sequence . poca conducta". Nadie notó ese problema hasta el número 9213 . El parche para ese problema no solo agregó index y count al range de 3.2, sino que también reelaboró el __contains__ optimizado (que comparte las mismas matemáticas con index y es utilizado directamente por count ). ** Este cambio también se realizó en 3.2 y no se retroportó a 2.x, porque "es una corrección de errores que agrega nuevos métodos". (En este punto, 2.7 ya había pasado el estado rc).

Por lo tanto, hubo dos posibilidades de que esta optimización se volviera a 2.7, pero ambas fueron rechazadas.


* De hecho, incluso obtiene la iteración de forma gratuita solo con la indexación, pero en 2.3 los objetos xrange obtuvieron un iterador personalizado.

** La primera versión en realidad lo reimplementó y se equivocó en los detalles; por ejemplo, le daría MyIntSubclass(2) in range(5) == False . Pero la versión actualizada del parche de Daniel Stutzbach restauró la mayor parte del código anterior, incluido el respaldo al genérico y lento _PySequence_IterSearch que el range.__contains__ se usaba implícitamente cuando la optimización no se aplicaba.

over 4 years ago · Santiago Trujillo Denunciar

0

Pruebe x-1 in (i for i in range(x)) para valores grandes de x , que utiliza un generador de comprensión para evitar invocar la optimización de range.__contains__ .

over 4 years ago · Santiago Trujillo Denunciar

0

  1. Debido a la optimización, es muy fácil comparar números enteros dados solo con el rango mínimo y máximo.
  2. La razón por la que la función range() es tan rápida en Python3 es que aquí usamos un razonamiento matemático para los límites, en lugar de una iteración directa del objeto range.
  3. Entonces, para explicar la lógica aquí:
  • Compruebe si el número está entre el inicio y el final.
  • Compruebe si el valor de precisión del paso no supera nuestro número.
  1. Tome un ejemplo, 997 está en el rango (4, 1000, 3) porque:

    4 <= 997 < 1000, and (997 - 4) % 3 == 0.

over 4 years ago · Santiago Trujillo Denunciar

0

TL;DR

El objeto devuelto por range() es en realidad un objeto de range . Este objeto implementa la interfaz del iterador para que pueda iterar sobre sus valores secuencialmente, como un generador, una lista o una tupla.

Pero también implementa la interfaz __contains__ que en realidad es lo que se llama cuando aparece un objeto en el lado derecho del operador in . El __contains__() devuelve un bool de si el elemento en el lado izquierdo de la in está o no en el objeto. Dado que los objetos range conocen sus límites y su paso, esto es muy fácil de implementar en O(1).

over 4 years ago · Santiago Trujillo Denunciar

0

Se trata de un enfoque perezoso para la evaluación y alguna optimización extra de range . Los valores en los rangos no necesitan calcularse hasta el uso real, o incluso más debido a la optimización adicional.

Por cierto, su número entero no es tan grande, considere sys.maxsize

sys.maxsize in range(sys.maxsize) es bastante rápido

debido a la optimización: es fácil comparar números enteros dados solo con el rango mínimo y máximo.

pero:

Decimal(sys.maxsize) in range(sys.maxsize) es bastante lento .

(en este caso, no hay optimización en el range , por lo que si python recibe un decimal inesperado, python comparará todos los números)

Debe estar al tanto de los detalles de implementación, pero no se debe confiar en ellos, ya que esto puede cambiar en el futuro.

over 4 years ago · Santiago Trujillo Denunciar

0

TLDR; el range es una serie aritmética, por lo que puede calcular muy fácilmente si el objeto está allí. Incluso podría obtener el índice si fuera una lista muy rápida.

over 4 years ago · Santiago Trujillo Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda